﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Skugo.Core;
using Skugo.Shared;
using System.Windows.Media.Imaging;
using System.Windows;
using System.Threading;
using System.Windows.Threading;
using System.Windows.Input;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Windows.Interop;
using System.Windows.Forms.Integration;
using System.Diagnostics;
using System.Windows.Media;
using Microsoft.Xna.Framework.Content;
using XnaMatrix = Microsoft.Xna.Framework.Matrix;
using XnaColor = Microsoft.Xna.Framework.Color;
using System.Windows.Controls;
using AvalonDock;
using Microsoft.Xna.Framework.Input;

namespace Skugo.StandardPlugins
{
	// The viewport is a basic plugin that gives the user a view into the world
	public class Viewport : DockablePlugin, IServiceProvider
    {
        Vector2? LastMousePosition;

        Texture2D RemoteFrame;

        SpriteBatch Sprites;

        /// <summary>
        /// The type of message we send or receieve.
        /// </summary>
        enum MessageType : byte
        {
            ReadyForFrameBuffer,
            FrameBuffer,
            MouseEvent,
            MouseButtonEvent,
            MouseWheelEvent,
            KeyboardState,
            WorldToProjection,
            DrawLineSegment,
            LocalWindow,
            FramePassed,
        }

        private const Int32 BytesPerColor = sizeof(UInt16);

		// Create a viewport form that we use to show the viewport
		private ViewportForm Form;
        private GraphicsDeviceControl Renderer;
        private DebugDrawer Drawer;
        private SpriteFont Font;

        private Int32 ImageFrameRate;
        private Single ImageFrameRateTimer;
        private Int32 ImageFrameRateCounter;
        private Stopwatch Timer = new Stopwatch();

        private XnaMatrix WorldToProjection = XnaMatrix.Identity;

        private ContentManager XnaContent;

        protected override void Initialize(BinaryReader reader)
        {
            base.Initialize(reader);

            // Create the form itself
            this.Form = new ViewportForm();
            this.Renderer = new GraphicsDeviceControl();

            this.Renderer.RenderXna += this.OnRender;
            this.Renderer.LoadContent += this.OnDeviceLoaded;

            this.Content = this.Form;

            this.Renderer.HwndMouseMove += (sender, e) => this.SendMouseEvent(e, MessageType.MouseEvent, MouseButton.Left, MouseButtonState.Released, 0);
            this.Renderer.HwndLButtonDown += (sender, e) => this.SendMouseDownEvent(e, MessageType.MouseButtonEvent, MouseButton.Left, 0);
            this.Renderer.HwndLButtonUp += (sender, e) => this.SendMouseUpEvent(e, MessageType.MouseButtonEvent, MouseButton.Left, 0);
            this.Renderer.HwndRButtonDown += (sender, e) => this.SendMouseDownEvent(e, MessageType.MouseButtonEvent, MouseButton.Right, 0);
            this.Renderer.HwndRButtonUp += (sender, e) => this.SendMouseUpEvent(e, MessageType.MouseButtonEvent, MouseButton.Right, 0);
            this.Renderer.HwndMButtonDown += (sender, e) => this.SendMouseDownEvent(e, MessageType.MouseButtonEvent, MouseButton.Middle, 0);
            this.Renderer.HwndMButtonUp += (sender, e) => this.SendMouseUpEvent(e, MessageType.MouseButtonEvent, MouseButton.Middle, 0);
            this.Renderer.HwndMouseWheel += (sender, e) => this.SendMouseEvent(e, MessageType.MouseWheelEvent, MouseButton.Left, MouseButtonState.Released, e.WheelDelta);
            ////this.Renderer.hwnd WHEEL

            this.Renderer.AllowDrop = true;
            this.Renderer.Focus();

            this.Form.Panel.Children.Add(this.Renderer);

            this.Form.Panel.Drop += this.OnDrop;

            this.Timer.Start();

            this.SendReadyMessage();
        }

        void OnDrop(object sender, DragEventArgs e)
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Get the position of the drag drop event
            var point = e.GetPosition(this.Form.Panel);

            // Normalize the coordinates [0, 1] form
            var position = new Vector2(
                (Single)(point.X / this.Form.Panel.ActualWidth),
                (Single)(point.Y / this.Form.Panel.ActualHeight));

            // Send over the coordinates of the drag drop
            outgoing.Write(position.X);
            outgoing.Write(position.Y);

            // Send the drag drop event
            this.SendDragDrop(stream.ToArray(), e);
        }

        private void SendMouseDownEvent(HwndMouseEventArgs e, MessageType type, MouseButton button, Int32 wheelDelta)
        {
            //this.Renderer.CaptureMouse();
            this.SendMouseEvent(e, type, button, MouseButtonState.Pressed, wheelDelta);
        }

        private void SendMouseUpEvent(HwndMouseEventArgs e, MessageType type, MouseButton button, Int32 wheelDelta)
        {
            this.SendMouseEvent(e, type, button, MouseButtonState.Released, wheelDelta);
            //this.Renderer.ReleaseMouseCapture();
        }

        private void SendMouseEvent(HwndMouseEventArgs e, MessageType type, MouseButton button, MouseButtonState state, Single wheelDelta)
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the mouse event message
            outgoing.Write((Byte)type);

            if (type == MessageType.MouseButtonEvent)
            {
                outgoing.Write(Convert.ToBoolean(state));
                outgoing.Write((Byte)button);
            }
            else if (type == MessageType.MouseWheelEvent)
            {
                // Write out a the mouse event message
                outgoing.Write((Single)wheelDelta);
            }

            outgoing.Write(Convert.ToBoolean(e.LeftButton));
            outgoing.Write(Convert.ToBoolean(e.MiddleButton));
            outgoing.Write(Convert.ToBoolean(e.RightButton));

            outgoing.Write(Convert.ToBoolean(System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.LeftShift)));
            outgoing.Write(Convert.ToBoolean(System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.RightShift)));
            outgoing.Write(Convert.ToBoolean(System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.LeftCtrl)));
            outgoing.Write(Convert.ToBoolean(System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.RightCtrl)));
            outgoing.Write(Convert.ToBoolean(System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.LeftAlt)));
            outgoing.Write(Convert.ToBoolean(System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.RightAlt)));

            var position = new Vector2(
                (Single)(e.Position.X / this.Form.View.ActualWidth),
                (Single)(e.Position.Y / this.Form.View.ActualHeight));

            outgoing.Write(position.X);
            outgoing.Write(position.Y);

            if (this.LastMousePosition == null)
            {
                outgoing.Write(0.0f);
                outgoing.Write(0.0f);
            }
            else
            {
                var lastPosition = this.LastMousePosition.Value;
                outgoing.Write(position.X - lastPosition.X);
                outgoing.Write(position.Y - lastPosition.Y);
            }

            this.LastMousePosition = position;

            this.Send(stream.ToArray());
        }

        void OnDeviceLoaded(object sender, GraphicsDeviceEventArgs e)
        {
            this.Sprites = new SpriteBatch(e.GraphicsDevice);
            this.RemoteFrame = new Texture2D(e.GraphicsDevice, 800, 480, false, SurfaceFormat.Bgr565);

            var exePath = Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);

            this.XnaContent = new ContentManager(this, exePath + @"\Content");

            this.Font = this.XnaContent.Load<SpriteFont>("Default");

            var debugDrawEffect = this.XnaContent.Load<Effect>("DebugDraw");
            this.Drawer = new DebugDrawer(e.GraphicsDevice, debugDrawEffect);

            DispatcherTimer dispatcherTimer = new System.Windows.Threading.DispatcherTimer();
            dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick);
            dispatcherTimer.Interval = new TimeSpan(0,0,0,0,1);
            dispatcherTimer.Start();
        }

        void dispatcherTimer_Tick(object sender, EventArgs e)
        {
            this.Renderer.Render();
        }

        void OnRender(object sender, GraphicsDeviceEventArgs e)
        {
            e.GraphicsDevice.Clear(XnaColor.Black);

            this.Sprites.Begin(SpriteSortMode.Immediate, BlendState.Opaque);

            var width = (int)this.Form.Panel.ActualWidth;
            var height = (int)this.Form.Panel.ActualHeight;
            
            var rect = new Microsoft.Xna.Framework.Rectangle(0, 0, width, height);

            this.Sprites.Draw(this.RemoteFrame, rect, XnaColor.White);

            this.Sprites.End();

            this.Drawer.DrawAll(e.FrameTime, this.WorldToProjection);

            this.Sprites.Begin();
            this.Sprites.DrawString(this.Font, "FPS: " + e.FrameRate, new Vector2(0.0f, 0.0f), XnaColor.Blue);
            this.Sprites.DrawString(this.Font, "IPS: " + this.ImageFrameRate, new Vector2(0.0f, this.Font.LineSpacing), XnaColor.Maroon);
            this.Sprites.End();

            this.SendKeyboardState();
        }

        private void SendKeyboardState()
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the ready message
            outgoing.Write((Byte)MessageType.KeyboardState);


            var kb = Microsoft.Xna.Framework.Input.Keyboard.GetState();
            var keys = kb.GetPressedKeys();

            outgoing.Write(keys.Length);

            for (Int32 i = 0; i < keys.Length; ++i)
            {
                outgoing.Write((Byte)keys[i]);
            }

            this.Send(stream.ToArray());
        }

        private void SendReadyMessage()
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out a the ready message
            outgoing.Write((Byte)MessageType.ReadyForFrameBuffer);

            this.Send(stream.ToArray());
        }


        private void LoadImage(GraphicsDevice device, BinaryReader incoming)
        {
            if (device != null)
            {
                var width = incoming.ReadInt32();
                var height = incoming.ReadInt32();

                this.Form.Panel.Width = width;
                this.Form.Panel.Height = height;

                var frameBuffer = incoming.ReadBytes(width * height * BytesPerColor);

                if (this.RemoteFrame.Width != width || this.RemoteFrame.Height != height)
                {
                    this.RemoteFrame.Dispose();
                    this.RemoteFrame = new Texture2D(device, width, height, false, SurfaceFormat.Bgr565);
                }

                if (device.Textures[0] == this.RemoteFrame)
                {
                    device.Textures[0] = null;
                }

                this.RemoteFrame.SetData(frameBuffer);

                this.Renderer.Render();

                this.ImageFrameRateTimer += (Single)this.Timer.Elapsed.TotalSeconds;
                this.Timer.Restart();
                ++this.ImageFrameRateCounter;

                if (this.ImageFrameRateTimer > 1.0f)
                {
                    this.ImageFrameRate = this.ImageFrameRateCounter;
                    this.ImageFrameRateCounter = 0;
                    this.ImageFrameRateTimer -= 1.0f;
                }
            }

            this.SendReadyMessage();
        }

        private DebugDrawInfo ReadDebugDrawInfo(BinaryReader incoming)
        {
            var info = new DebugDrawInfo();
            info.Color.PackedValue = incoming.ReadUInt32();
            info.Duration = incoming.ReadSingle();
            info.Priority = incoming.ReadInt32();
            info.Space = (TransformSpace)incoming.ReadByte();
            return info;
        }

        protected override void OnReceive(Byte[] data)
		{
            var incoming = data.ToBinaryReader();

            var messageType = (MessageType)incoming.ReadByte();

            switch (messageType)
            {
                case MessageType.FrameBuffer:
                {
                    this.LoadImage(this.Renderer.Device, incoming);
                    break;
                }

                case MessageType.WorldToProjection:
                {
                    this.WorldToProjection = incoming.ReadMatrix();
                    break;
                }

                case MessageType.LocalWindow:
                {
                    var handle = (IntPtr)incoming.ReadInt32();
                    //this.HijackLocalWindow(handle);
                    break;
                }

                case MessageType.DrawLineSegment:
                {
                    var info = this.ReadDebugDrawInfo(incoming);
                    var p1 = incoming.ReadVector3();
                    var p2 = incoming.ReadVector3();
                    var arrow1 = incoming.ReadSingle();
                    var arrow2 = incoming.ReadSingle();

                    this.Drawer.DrawLineSegment(p1, p2, info.Color);
                    break;
                }

                case MessageType.FramePassed:
                {
                    if (this.Drawer != null)
                    {
                        this.Drawer.SwapDebugList();
                    }
                    break;
                }

                default:
                    throw new Exception("Unhandled message type!");
            }
		}

        private void HijackLocalWindow(IntPtr handle)
        {
            var style = (UInt32)WindowHelpers.GetWindowLong(handle, WindowHelpers.GWL_STYLE);

            style |= WindowStyles.WS_CHILD;
            style &= ~WindowStyles.WS_POPUP;
            style &= ~WindowStyles.WS_BORDER;
            style &= ~WindowStyles.WS_CAPTION;
            style &= ~WindowStyles.WS_DLGFRAME;
            style &= ~WindowStyles.WS_MAXIMIZE;
            style &= ~WindowStyles.WS_MAXIMIZEBOX;
            style &= ~WindowStyles.WS_MINIMIZE;
            style &= ~WindowStyles.WS_MINIMIZEBOX;
            style &= ~WindowStyles.WS_SIZEBOX;
            style &= ~WindowStyles.WS_TABSTOP;


            WindowHelpers.SetWindowLong(handle, WindowHelpers.GWL_STYLE, (int)style);

            WindowHelpers.SetParent(handle, this.Renderer.Handle);

            RECT rect;
            WindowHelpers.GetWindowRect(handle, out rect);

            WindowHelpers.SetWindowPos(handle, (IntPtr)SpecialWindowHandles.HWND_TOP, 0, 0, rect.Right - rect.Left, rect.Bottom - rect.Top, SetWindowPosFlags.SWP_SHOWWINDOW | SetWindowPosFlags.SWP_ASYNCWINDOWPOS);
        }

        public override void Dispose()
        {
            this.Renderer.Dispose();
            base.Dispose();
        }

        public object GetService(Type serviceType)
        {
            // We only implement the graphics service
            return this.Renderer.GraphicsService;
        }
    }
}
